昨天認識了組合語言與電腦間的基本架構,今天要繼續介紹組合語言的指令語法和暫存器等等,一樣會以 x86 架構來介紹,不過因為這本書太古老了所以是用 32-bit 來介紹,基本上概念是一樣的!後面會附 x86-64 的參考資料
指令是組合語言程式的基本構成元素。在 x86 組合語言中,一條指令由一個 mnemonic 以及零個或多個 operands 組成。mnemonic 是一個用來識別要執行哪種指令的單字,例如 mov,代表移動資料。operands 通常用來指定該指令要使用的資訊,例如暫存器或資料。

以上圖為例
mov → mnemonic:告訴 CPU 我要搬資料ecx → destination operand:目的地暫存器0x42 → source operand:要搬過去的資料所以意思就是告訴 CPU 要把 0x42 放進 ecx 暫存器
每一條指令都對應到 opcodes(operation codes),它會告訴 CPU 程式想要執行哪一種操作,嚴格來說 Intel 對 opcode 的定義範圍沒有這麼廣,不過這邊根據教材以 opcode 這個詞來指稱整個 machine instruction。反組譯器(disassembler)會將 opcode 轉換成人類可讀的指令。

像是上途中,可以看到指令 mov ecx, 0x42 對應的 opcode 為 B9 42 00 00 00。其中,0xB9 對應到 mov ecx,而 0x42000000 則對應到數值 0x42。
之所以會將 0x42000000 視為數值 0x42,是因為 x86 架構使用 little-endian 格式。所謂的 endianness 指的是在一個由多個位元組組成的較大資料中,是最高有效位元組(most significant byte,big-endian)還是最低有效位元組(least significant byte,little-endian)會被排列在最前面,也就是放在最小的記憶體位址。
以上聽起來很難理解,所以這邊用一個 32-bit 數值來舉例,假設我們有:
0x12345678
然後可以把它拆成 4 bytes 來看
如果是 Big-endian,記憶體會按照我們平常閱讀數字的順序存:
低位址 → 高位址
12 34 56 78
但 x86 使用 little-endian,所以順序會反過來:
低位址 → 高位址
78 56 34 12
所以剛剛一開始看到的
0x00000042
用 little-endian 來看就是
42 00 00 00
因為網路資料使用 big-endian,而 x86 程式使用 little-endian,所以惡意程式在進行網路通訊時,必須處理不同位元組序之間的轉換。因此,IP 位址 127.0.0.1 在 big-endian 格式中(透過網路傳輸時)會表示成 0x7F000001,而在 little-endian 格式中(本機記憶體內)則會表示成 0x0100007F。因為有這樣的差異,所以在做惡意程式分析的時候必須特別注意位元組序以免順序顛倒。
operands 是用來指定一條指令所使用的資料,可以分成三種類型:
是固定的數值,例如剛剛的 0x42,舉例來說
mov eax, 0x42
就是直接把固定數值 0x42 放進 eax
指的是暫存器,例如剛剛的 ecx,舉例來說
mov eax, ecx
就是把 ecx 裡面的值複製到 eax
指的是一個記憶體位址,通常會使用中括號包住一個數值、暫存器或運算式來表示,例如 [eax]。這個概念很容易跟上一個的 eax 搞混,他的概念上很像指標,我們用 [eax] 並不是去取 eax 本身的值,而是用它本身的值當作地址,去那個記憶體位址取得資料。聽起來很抽象,所以以下舉例說明:
假設程式向作業系統要了一塊記憶體,並且得知
我給你的記憶體從:
0x00500000 開始
為了之後操作方便,程式把這個地址記在 eax
eax = 0x00500000
接著把資料寫進記憶體
mov [eax], 42
意思是把 42 這個值放進 eax 所指向的地址,也就是剛剛的 0x00500000,所以現在 0x00500000 這個地址存放的值為 42,也就是
eax = 0x00500000
│
↓
記憶體
0x00500000 [ 42 ]
之後我們要拿這裡面的資料就可以用
mov ebx, [eax]
也就是將 eax 當成地圖,找到東西再把它複製到 ebx
eax = 0x00500000
│
↓
[ 42 ]
│
↓
ebx = 42
暫存器是 CPU 可以使用的一小塊資料儲存空間,其中的內容可以比其他地方的儲存空間更快速地被存取,x86 處理器具有一組暫存器,可以作為暫時性的資料儲存空間或工作空間使用,以下是最常見的 x86 暫存器

它們可以分成以下四種類型:
這些暫存器大小都是 32 bits,而在組合語言程式碼中,可以使用 32 bits 或 16 bits 的形式來存取它們。例如,edx 用來表示完整的 32-bit 暫存器,而 dx 則用來表示 edx 暫存器較低的 16 bits。其中四個暫存器(eax、ebx、ecx 和 edx)還可以使用 8-bit 的形式來存取,也就是存取最低的 8 bits,或是緊接著的第二組 8 bits。例如,al 用來表示 eax 暫存器最低的 8 bits,而 ah 則用來表示第二組 8 bits。

一般暫存器通常用來儲存資料或記憶體位址,而且在程式執行過程中,為了完成各種工作,這些暫存器經常可以互相替換使用。然而,儘管它們被稱為「一般暫存器」,實際上並不總是能如此自由地使用。
有些 x86 指令在定義上就規定必須使用特定的暫存器。例如,乘法與除法指令總是會使用 eax 和 edx。除了指令本身的規定之外,一般暫存器在整個程式中也可能按照一致的方式使用。在編譯後的程式碼中,以一致方式使用暫存器的規則稱為 convention。
了解編譯器所使用的這些 convention,可以讓惡意程式分析人員更快速地檢查程式碼,因為不需要每次都花時間重新判斷某個暫存器在目前情境下是用來做什麼的。例如,eax 通常會保存 function call 的回傳值。因此,如果你看到程式在呼叫某個函式之後立刻使用 eax 暫存器,那麼你看到的很可能就是程式正在處理該函式的回傳值。
| 指令 | 意思 | 範例 | 解釋 |
|---|---|---|---|
mov |
複製資料 | mov eax, ebx |
eax = ebx |
lea |
取得地址 | lea eax, [ebx+4] |
eax = ebx+4 這個地址 |
xchg |
交換 | xchg eax, ebx |
交換 eax、ebx |
movzx |
搬移並補 0 | movzx eax, al |
把 al 放進 eax,其餘高位補 0 |
movsx |
搬移並做符號擴展 | movsx eax, al |
把有號 al 擴展成 32-bit |
| 指令 | 意思 | 範例 | 結果 |
|---|---|---|---|
add |
加 | add eax, 5 |
eax = eax + 5 |
sub |
減 | sub eax, 5 |
eax = eax - 5 |
inc |
+1 | inc eax |
eax ++ |
dec |
-1 | dec eax |
eax -- |
imul |
有號乘法 | imul eax, 5 |
eax × 5 |
mul |
無號乘法 | mul ebx |
乘法,隱含使用 eax /edx |
idiv |
有號除法 | idiv ebx |
除法 |
div |
無號除法 | div ebx |
除法 |
neg |
取負數 | neg eax |
eax = -eax |
| 指令 | 意思 |
|---|---|
and |
AND |
or |
OR |
xor |
XOR |
not |
NOT |
test |
AND,但只修改 Flags,不保存結果 |
因為這本書寫的時候主流電腦架構還是 x86,所以照著書上的先簡單介紹,x86-64 的參考資料附在下面,裡面也有實際的範例可以練習!認識了組語之後分析惡意程式的時候反組譯完就可以輕鬆看懂啦!
